﻿using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Collections.Generic;
using System;

namespace block_explorer
{
    class DeferredRenderer : DrawableGameComponent
    {
        private Scene mScene;

        private GBuffer mGBuffer;
        private RenderTarget2D mLightRT;
        private RenderTarget2D m2DRT;

        private Shaders.Render2DEffect m2DEffect;
        private Shaders.CombineEffect mCombine;
        private Shaders.ShadowMapEffect mShadowMapEffect;

        private Effect mRenderPostProcessEffect;

        private DirectionalLightRenderer mDirectionalLightRenderer;
        private PointLightRenderer mPointLightRenderer;
        private SpotLightRenderer mSpotLightRenderer;

        private Vector2 mGBufferHalfPixel;

        private IList<IPostProcess> mPostProcesses;

        private Util.QuadRenderComponent mQuadRenderer;
        private Util.DebugTextureTiler mDebugTextureTiler;

        public Scene Scene { get { return mScene; } }
        public GBuffer GBuffer { get { return mGBuffer; } }
        public Texture2D LightMap { get { return mLightRT; } }
        public Vector2 GBufferHalfPixel { get { return mGBufferHalfPixel; } }

        public bool DebugTextureRender { get { return mDebugTextureTiler.Visible; } set { mDebugTextureTiler.Visible = value; } }

        public DeferredRenderer(BlockExplorer game, Scene scene)
            :base(game)
        {
            mScene = scene;

            mGBuffer = new GBuffer();

            mPostProcesses = new List<IPostProcess>();

            mDirectionalLightRenderer = new DirectionalLightRenderer();
            mPointLightRenderer = new PointLightRenderer();
            mSpotLightRenderer = new SpotLightRenderer();
        }

        public override void Initialize()
        {
            mQuadRenderer = new Util.QuadRenderComponent(Game);
            Game.Components.Add(mQuadRenderer);

            mDebugTextureTiler = new Util.DebugTextureTiler(Game, 150, 150, new Rectangle(0, 0, Game.Window.ClientBounds.Width, Game.Window.ClientBounds.Height));
            Game.Components.Add(mDebugTextureTiler);
            mDebugTextureTiler.Visible = false;

            base.Initialize();
        }

        public void AddPostProcess(IPostProcess process)
        {
            mPostProcesses.Add(process);
        }

        protected override void LoadContent()
        {
            int bufferWidth = GraphicsDevice.PresentationParameters.BackBufferWidth;
            int bufferHeight = GraphicsDevice.PresentationParameters.BackBufferHeight;

            // prepare the buffers for our effects
            mGBuffer.Prepare(GraphicsDevice, Game.Content, bufferWidth, bufferHeight);
            mLightRT = new RenderTarget2D(GraphicsDevice, bufferWidth, bufferHeight, false, SurfaceFormat.Color, DepthFormat.None);
            m2DRT = new RenderTarget2D(GraphicsDevice, bufferWidth, bufferHeight, false, SurfaceFormat.Color, DepthFormat.None);
            
            m2DEffect = new Shaders.Render2DEffect(GraphicsDevice, Game.Content);
            mCombine = new Shaders.CombineEffect(GraphicsDevice, Game.Content);
            mShadowMapEffect = new Shaders.ShadowMapEffect(GraphicsDevice, Game.Content);
            mRenderPostProcessEffect = Game.Content.Load<Effect>("Effects/PlainRender");

            mGBufferHalfPixel.X = 0.5f / (float)bufferWidth;
            mGBufferHalfPixel.Y = 0.5f / (float)bufferHeight;

            // prepare the light renderers
            mDirectionalLightRenderer.Prepare(GraphicsDevice, Game.Content);
            mPointLightRenderer.Prepare(GraphicsDevice, Game.Content);
            mSpotLightRenderer.Prepare(GraphicsDevice, Game.Content);

            foreach (IPostProcess process in mPostProcesses)
            {
                process.Initialize(GraphicsDevice, Game.Content);
            }

            // add the different textures to a debug helper
            mDebugTextureTiler.AddTexture(mGBuffer.Color);
            mDebugTextureTiler.AddTexture(mGBuffer.Normal);
            mDebugTextureTiler.AddTexture(mGBuffer.Depth);
            mDebugTextureTiler.AddTexture(mLightRT);
            foreach (IPostProcess process in mPostProcesses)
            {
                if (process.Surface != null)
                    mDebugTextureTiler.AddTexture(process.Surface);
            }
            mDebugTextureTiler.AddTexture(m2DRT);
            foreach (BaseSceneLight light in Scene.Lights)
            {
                if (light.Shadows)
                {
                    light.PrepareShadowMap(GraphicsDevice);
                    Texture2D tex = light.ShadowMap as Texture2D;
                    if (tex != null)
                        mDebugTextureTiler.AddTexture(tex);
                }
            }

            base.LoadContent();
        }

        public override void Update(GameTime gameTime)
        {
            base.Update(gameTime);
        }

        public override void Draw(GameTime gameTime)
        {
            // first fill the gbuffer
            mGBuffer.Fill(GraphicsDevice, mScene);

            // render out the 2d scene elements to a separate buffer
            Draw2DElements();

            // light occlusion maps for shadowing lights
            foreach (BaseSceneLight light in Scene.Lights)
            {
                if (light.Shadows)
                {
                    light.DrawShadowMap(GraphicsDevice, mShadowMapEffect, Scene, Scene.SelectedCamera);
                }
            }

            // fill the lightmap
            DrawLights();

            // let the post processes do their thing
            foreach (IPostProcess process in mPostProcesses)
            {
                if (process.Enabled)
                    process.PrepareScene(GraphicsDevice);
            }

            // compose the final image using all the buffers
            ComposeScene();

            // render the 2d elements over the top
            Overlay2DElements();

            base.Draw(gameTime);
        }

        private void Draw2DElements()
        {
            GraphicsDevice.SetRenderTarget(m2DRT);

            GraphicsDevice.Clear(Color.Transparent);

            m2DEffect.DepthMap = mGBuffer.Depth;

            Scene.Draw2D(m2DEffect);
        }

        private void DrawLights()
        {
            GraphicsDevice.SetRenderTarget(mLightRT);

            GraphicsDevice.Clear(mScene.AmbientLight);

            // additive blending
            BlendState b = new BlendState();
            b.AlphaBlendFunction = BlendFunction.Add;
            b.ColorSourceBlend = Blend.One;
            b.ColorDestinationBlend = Blend.One;
            GraphicsDevice.BlendState = b;
            GraphicsDevice.DepthStencilState = DepthStencilState.None;

            foreach (BaseSceneLight light in mScene.Lights)
            {
                DirectionalLight directionalLight = light as DirectionalLight;
                PointLight pointLight = light as PointLight;
                SpotLight spotLight = light as SpotLight;

                if (directionalLight != null)
                {
                    mDirectionalLightRenderer.RenderLight(directionalLight, Scene.SelectedCamera, this);
                }
                else if (pointLight != null)
                {
                    mPointLightRenderer.RenderLight(pointLight, Scene.SelectedCamera, this);
                }
                else if (spotLight != null)
                {
                    mSpotLightRenderer.RenderLight(spotLight, Scene.SelectedCamera, this);
                }
            }

            GraphicsDevice.BlendState = BlendState.Opaque;
        }

        private void ComposeScene()
        {
            GraphicsDevice.SetRenderTarget(null);

            mCombine.Combine(mGBuffer.Color, mLightRT, mGBufferHalfPixel);

            foreach (IPostProcess process in mPostProcesses)
            {
                if (process.Enabled)
                {
                    process.Draw(GraphicsDevice);
                }
            }
        }

        private void Overlay2DElements()
        {
            GraphicsDevice.BlendState = BlendState.AlphaBlend;
            GraphicsDevice.Textures[0] = m2DRT;
            mRenderPostProcessEffect.CurrentTechnique.Passes[0].Apply();
            Util.QuadRenderComponent.Instance.Render(-1 * Vector2.One, Vector2.One);
        }
    }
}
